/*******************************************************************************
 * Public Static Functions
 ******************************************************************************/

/**
 * Creates an instance of an e34 object, mapped to a page Element
 * @constructor
 * @param {Element} element The element to wrap with an e34 object.
 */
var e34 = function(element, opt_assert) {
  opt_assert = (opt_assert == undefined) ? true : opt_assert;
  this.element_ = element;
  if (opt_assert == true) {
    this.assertElement_();
  }
};

/**
 * Returns an e34 object corresponding to the supplied id, Element, or e34 
 * object.
 * @param {String|Element|e34} id If a string, the ID of the DOM Element to 
 *     wrap.  If an Element, the Element to wrap.  If an e34 object, the object
 *     is just returned. 
 * @return {e34} An e34 object if the supplied argument pointed to a valid 
 *     element, or else null.
 */
e34.$ = function(id) {
  if (e34.isString(id)) {
    return new e34(document.getElementById(id));
  } else if (e34.isElement(id)) {
    return new e34(id);
  } else if (e34.isE34_(id)) {
    return id;
  } else {
    return null;
  }
};

/**
 * Returns an array of e34 objects corresponding with the supplied class name
 * 
 * Originally developed by Robert Nyman, http://www.robertnyman.com
 * Code/licensing: http://code.google.com/p/getelementsbyclassname/
 * Modified by Arne Roomann-Kurrik for e34.
 *
 * @param {String} class_name The class name of the objects to return.
 * @param {String} tag_name The tag name of the elements to return.  Defaults
 *     to "*", which will select all elements.
 * @param {String|Element|e34} root The base node from which to start the 
 *     search.  Can be any of the types that e34.$ recognizes.
 * @return {Array} An array of e34 objects, if any were found matching the 
 *     supplied arguments.  Otherwise, returns an empty array.
 */
e34.$C = function (className, tag, elm){
  if (document.getElementsByClassName) {
    getElementsByClassName = function (className, tag, elm) {
      elm = elm || document;
      var elements = elm.getElementsByClassName(className);
      var nodeName = (tag)? new RegExp("\\b" + tag + "\\b", "i") : null;
      var returnElements = [];
      var current;
      for(var i=0, il=elements.length; i<il; i+=1){
        current = elements[i];
        if(!nodeName || nodeName.test(current.nodeName)) {
          returnElements.push(e34.$(current));
        }
      }
      return returnElements;
    };
  } else if (document.evaluate) {
    getElementsByClassName = function (className, tag, elm) {
      tag = tag || "*";
      elm = elm || document;
      var classes = className.split(" ");
      var classesToCheck = "";
      var xhtmlNamespace = "http://www.w3.org/1999/xhtml";
      var namespaceResolver = (document.documentElement.namespaceURI === xhtmlNamespace)? xhtmlNamespace : null;
      var returnElements = [];
      var elements;
      var node;
      for(var j=0, jl=classes.length; j<jl; j+=1){
        classesToCheck += "[contains(concat(' ', @class, ' '), ' " + classes[j] + " ')]";
      }
      try {
        elements = document.evaluate(".//" + tag + classesToCheck, elm, namespaceResolver, 0, null);
      }
      catch (e) {
        elements = document.evaluate(".//" + tag + classesToCheck, elm, null, 0, null);
      }
      while ((node = elements.iterateNext())) {
        returnElements.push(e34.$(node));
      }
      return returnElements;
    };
  } else {
    getElementsByClassName = function (className, tag, elm) {
      tag = tag || "*";
      elm = elm || document;
      var classes = className.split(" ");
      var classesToCheck = [];
      var elements = (tag === "*" && elm.all)? elm.all : elm.getElementsByTagName(tag);
      var current;
      var returnElements = [];
      var match;
      for(var k=0, kl=classes.length; k<kl; k+=1){
        classesToCheck.push(new RegExp("(^|\\s)" + classes[k] + "(\\s|$)"));
      }
      for(var l=0, ll=elements.length; l<ll; l+=1){
        current = elements[l];
        match = false;
        for(var m=0, ml=classesToCheck.length; m<ml; m+=1){
          match = classesToCheck[m].test(current.className);
          if (!match) {
            break;
          }
        }
        if (match) {
          returnElements.push(e34.$(current));
        }
      }
      return returnElements;
    };
  }
  if (elm) {
    elm = e34.$(elm).getElement();
  }
  return getElementsByClassName(className, tag, elm);
};

/**
 * Creates an instance of a template, performing token substitution, and 
 * returns an e34 object pointing to the new instance.
 * @param {String|Element|e34} template The template to instantiate.
 * @param {Object} data The data to use to perform token substitution.  Values
 *     in this object will be substituted for tokens in the template matching
 *     the literal \@xxx\@, where xxx is the value's key in this object.
 * @return {e34} A new instance of the template with substitutions performed.
 *     Note that if the template contained an ID on the root element, this ID
 *     will be removed from the rendered template.
 */
e34.renderTemplate = function(template, data) {
  if (e34.isString(template)) {
    return e34.renderStringTemplate_(template, data);
  } else if (e34.isElement(template) || e34.isE34_(template)) {
    return e34.renderStringTemplate_(e34.serializeElement_(template), data);
  } else {
    return null;
  }
};

/*******************************************************************************
 * Private Static Functions
 ******************************************************************************/

/**
 * Converts an element to a string representation.
 * @param {Element|String|e34} element An element reference as accepted by e34.$
 *     representing the element to be converted to a string.
 * @return {String} A string that represents the given element.
 */
e34.serializeElement_ = function(element) {
  element = e34.$(element);
  var node = document.createElement("div");
  node.appendChild(element.getElement().cloneNode(true));
  return node.innerHTML;
};

/**
 * Creates an instance of a template, performing token substitution, and 
 * returns an e34 object pointing to the new instance.
 * @param {String} template The String template to instantiate.
 * @param {Object} data The data to use to perform token substitution.  Values
 *     in this object will be substituted for tokens in the template matching
 *     the literal \@xxx\@, where xxx is the value's key in this object.
 * @return {e34} A new instance of the template with substitutions performed.
 *     Note that if the template contained an ID on the root element, this ID
 *     will be removed from the rendered template.
 */
e34.renderStringTemplate_ = function(template, data) {
  for (var key in data) {
    if (data.hasOwnProperty(key)) {
      var replace_regexp = RegExp("@" + key + "@", "g");
      template = template.replace(replace_regexp, data[key]);
    }
  }
  var replace_root_id_regexp = /^<([^>]*)\s+(?:id='[^'>]*'|id="[^">]*"|id=[^"' >]*)(\s*[^>]*)>/;
  template = template.replace(replace_root_id_regexp, "<$1$2>");
  
  var node = null;
  var wrapper_node = document.createElement("div");
  
  if (template.match(/^<(?:tr|TR)/)) {
    wrapper_node.innerHTML = "<table><tbody>" + template + "</tbody></table>";
    node = wrapper_node.childNodes[0].childNodes[0].childNodes[0];
  } else if (template.match(/^<(?:option|OPTION)/)) {
    wrapper_node.innerHTML = "<select>" + template + "</select>";
    node = wrapper_node.childNodes[0].childNodes[0];    
  } else {
    wrapper_node.innerHTML = template;
    node = wrapper_node.childNodes[0];
  }

  if (node) {
    return new e34(node);
  } else {
    return null;
  }
};


e34.isString = function(string) {
  return (typeof string == "string");
};

e34.isElement = function(element) {
  return element && element.nodeType && element.nodeType == 1;
};

e34.isBoolean = function(bool) {
  return bool === true || bool === false;
};

e34.isNumber = function(number) {
  return !isNaN(number);
};

e34.isObject = function(obj) {
  return (typeof obj == typeof({}));
}

e34.isE34_ = function(element) {
  return element && element.e34version_;
};

e34.isArray = function(array) {
  return array && !!array.length;
};

e34.isFunction = function(func) {
  return (typeof func == "function");
};
/*******************************************************************************
 * Private Instance Methods
 ******************************************************************************/
e34.prototype.e34version_ = 1;

e34.prototype.assertElement_ = function() {
  if (e34.isElement(this.element_)) {
    return this;
  }
  throw new Error("e34 object was not assigned to an Element");  
};

/*******************************************************************************
 * Public Instance Methods
 ******************************************************************************/

e34.prototype.$C = function(class_name, tag_name) {
  return e34.$C(class_name, tag_name, this.element_);
};

e34.prototype.getId = function() {
  return this.element_.id;
};

e34.prototype.getClasses = function() {
  return this.element_.className.split(" ");
}
e34.prototype.getElement = function() {
  return this.element_;
};

e34.prototype.getTagName = function() {
  return this.element_.tagName.toUpperCase();
};

e34.prototype.getParentElement = function() {
  return this.element_.parentNode;
};

e34.prototype.getTemplateId = function() {
  var id_matches = /([^ ]+)-[0-9]+$/.exec(this.element_.id);
  if (id_matches) {
    return id_matches[1];
  } else {
    return this.element_.id;
  }  
};

e34.prototype.getTemplateNumber = function() {
  var id_matches = /-([0-9]+)$/.exec(this.element_.id);
  if (id_matches) {
    return Number(id_matches[1]);
  } else {
    return null;
  }
};

e34.prototype.getNumberedElement = function(id) {
  
};
  
e34.prototype.appendChildren = function() {
  for (var i = 0; i < arguments.length; i++) {
    this.element_.appendChild(e34.$(arguments[i]).getElement());
  }
  return this;
};

e34.prototype.setParent = function(parent) {
  e34.$(parent).getElement().appendChild(this.getElement());
  return this;
};

e34.prototype.removeFromParent = function() {
  this.element_.parentNode.removeChild(this.element_);
  return this;
};

e34.prototype.removeAllChildren = function() {
  this.element_.innerHTML = "";
  return this;
};

e34.prototype.replaceElement = function(element) {
  if (e34.isE34_(element)) {
    var parent = this.getParentElement();
    this.removeFromParent();
    parent.appendChild(element.getElement());
  }
  return this;
};

e34.prototype.setInnerHTML = function(html) {
  this.element_.innerHTML = html;
};

e34.prototype.hasClass = function(name) {
  var class_exists_regexp = RegExp("\\b" + name + "\\b");
  return this.element_.className && 
      !!this.element_.className.match(class_exists_regexp);
};

e34.prototype.addClass = function(name) {  
  var current_name = this.element_.className || "";
  var char_at_end_regexp = RegExp("(\\S$)");
  
  if (!this.hasClass(name)) {
    current_name = current_name.replace(char_at_end_regexp, "$1 ") + name;
  }
  this.element_.className = current_name;
  return this;
};

e34.prototype.removeClass = function(name) {
  var current_name = this.element_.className || "";
  var class_exists_regexp = RegExp("(\\s*\\b" + name + "\\b(\\s*))*","g");
  
  current_name = current_name.replace(class_exists_regexp, "$2");
  this.element_.className = current_name;
  return this;
};
     
e34.prototype.toggleClass = function(name) {
  if (this.hasClass(name)) {
    this.removeClass(name);
  } else {
    this.addClass(name);
  }
  return this;
};

e34.prototype.addListener = function(type, listener, capture) {
  if (!this.element_.addEventListener) {
    this.element_.attachEvent("on" + type, listener);
  } else {
    this.element_.addEventListener(type, listener, capture);
  }
};


e34.prototype.addOnClickHandler = function(handler) {
  this.addListener("click", handler, true);
  return this;
};

e34.prototype.hide = function() {
  if (this.element_.style.display != "none") {
    this.old_style_display_ = this.element_.style.display;
    LOG("setting old style display to", this.old_style_display);
    this.element_.style.display = "none";
  }
  return this;
};

e34.prototype.show = function() {
  LOG("show", this.element_, this.old_style_display_);
  if (this.element_.style.display == "none") {
    this.element_.style.display = this.old_style_display_;
  }
  return this;
};

e34.prototype.enable = function() {
  this.element_.disabled = false;
};

e34.prototype.disable = function() {
  this.element_.disabled = true;
}

e34.prototype.getValue = function() {
  if (this.element_.type && this.element_.type == "checkbox") {
    return this.element_.checked;
  } else {
    return this.element_.value;
  } 
};

e34.prototype.setValue = function(value) {
  if (this.element_.type && this.element_.type == "checkbox") {
    this.element_.checked = !!value;
  } else {
    this.element_.value = value;
  }   
  return this;
};

e34.prototype.getPosition = function() {
  var position = { "x": 0, "y": 0 };
  var pointer = this.element_;
  while( pointer ) {
    position.x += pointer.offsetLeft;
    position.y += pointer.offsetTop;
    pointer = pointer.offsetParent;
  }
  return position;
};

e34.prototype.getDimensions = function() {
  var dimensions = { 
      "width": this.element_.offsetWidth,
      "height": this.element_.offsetHeight 
  };
  return dimensions;
};

e34.prototype.getAttribute = function(name) {
  return this.element_.getAttribute(name);
};

e34.prototype.scrollIntoView = function() {
  this.element_.scrollIntoView(true);
  return this;
};

e34.prototype.$T = function(tag) {
  var elems = this.element_.getElementsByTagName(tag);
  var output = [];
  for (var i = 0, elem; elem = elems[i]; i++) {
    output.push(e34.$(elem));
  }
  return output;
};

e34.prototype.getChild = function(index) {
  return e34.$(this.element_.childNodes[index]);
};

e34.prototype.setStylePosition = function(position) {
  this.element_.style.position = position;
  return this;
};

e34.prototype.setStyleLeft = function(x) {
  this.element_.style.left = x;
  return this;
};

e34.prototype.setStyleTop = function(y) {
  this.element_.style.top = y;
  return this;
};

e34.prototype.setStyleWidth = function(width) {
  this.element_.style.width = width;
  return this;
};

e34.prototype.setStyleHeight = function(height) {
  this.element_.style.height = height;
  return this;
};

e34.prototype.setStyleMarginLeft = function(margin_left) {
  this.element_.style.marginLeft = margin_left;
  return this;
};

e34.prototype.setStylePaddingLeft = function(padding_left) {
  this.element_.style.paddingLeft = padding_left;
  return this;
};

e34.prototype.$ = function(id) {
  return e34.$(this.element_.ownerDocument.getElementById(id));
}

